/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.deltaspike.testcontrol.api.junit; import junit.framework.Assert; import org.apache.deltaspike.cdise.api.CdiContainer; import org.apache.deltaspike.cdise.api.CdiContainerLoader; import org.apache.deltaspike.cdise.api.ContextControl; import org.apache.deltaspike.core.api.projectstage.ProjectStage; import org.apache.deltaspike.core.api.provider.BeanManagerProvider; import org.apache.deltaspike.core.api.provider.BeanProvider; import org.apache.deltaspike.core.util.ExceptionUtils; import org.apache.deltaspike.core.util.ProjectStageProducer; import org.apache.deltaspike.core.util.ServiceUtils; import org.apache.deltaspike.testcontrol.api.TestControl; import org.apache.deltaspike.testcontrol.api.literal.TestControlLiteral; import org.apache.deltaspike.testcontrol.spi.ExternalContainer; import org.apache.deltaspike.testcontrol.spi.TestAware; import org.apache.deltaspike.testcontrol.spi.TestControlValidator; import org.apache.deltaspike.testcontrol.spi.junit.TestStatementDecoratorFactory; import org.junit.Test; import org.junit.internal.runners.statements.FailOnTimeout; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.context.RequestScoped; import javax.enterprise.context.SessionScoped; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.inject.Singleton; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.Stack; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; /** * A JUnit test runner to start up with a CDI or embedded JavaEE container. * * <p>If the underlying container supports it you can even pass in a property file * to the bootstrap. You can configure the name of the configuration file via a * {@link org.apache.deltaspike.core.api.config.ConfigResolver} property named * <code>cdiTestRunnerConfig</code>. By default the name of the config file is * <code>cdiTestRunnerConfig.properties</code>.</p> */ public class CdiTestRunner extends BlockJUnit4ClassRunner { private static final Logger LOGGER = Logger.getLogger(CdiTestRunner.class.getName()); private static final boolean USE_TEST_CLASS_AS_CDI_BEAN; private static final boolean ALLOW_INJECTION_POINT_MANIPULATION; private static Set<Integer> notifierIdentities = new CopyOnWriteArraySet<Integer>(); static { USE_TEST_CLASS_AS_CDI_BEAN = TestBaseConfig.ContainerIntegration.USE_TEST_CLASS_AS_CDI_BEAN; ALLOW_INJECTION_POINT_MANIPULATION = TestBaseConfig.MockIntegration.ALLOW_MANUAL_INJECTION_POINT_MANIPULATION; } private static ThreadLocal<Boolean> automaticScopeHandlingActive = new ThreadLocal<Boolean>(); private static ThreadLocal<CdiTestRunner> currentTestRunner = new ThreadLocal<CdiTestRunner>(); private List<TestStatementDecoratorFactory> statementDecoratorFactories; private ContainerAwareTestContext testContext; public CdiTestRunner(Class<?> testClass) throws InitializationError { super(testClass); TestControl testControl = testClass.getAnnotation(TestControl.class); this.testContext = new ContainerAwareTestContext(testControl, null); //benefits from the fallback-handling in ContainerAwareTestContext Class<? extends Handler> logHandlerClass = this.testContext.getLogHandlerClass(); if (!Handler.class.equals(logHandlerClass)) { try { LOGGER.addHandler(logHandlerClass.newInstance()); } catch (Exception e) { throw ExceptionUtils.throwAsRuntimeException(e); } } this.statementDecoratorFactories = ServiceUtils.loadServiceImplementations(TestStatementDecoratorFactory.class); Collections.sort(this.statementDecoratorFactories, new Comparator<TestStatementDecoratorFactory>() { @Override public int compare(TestStatementDecoratorFactory f1, TestStatementDecoratorFactory f2) { return f1.getOrdinal() > f2.getOrdinal() ? 1 : -1; } }); } @Override public void run(RunNotifier runNotifier) { if (!CdiTestSuiteRunner.isContainerStarted()) //not called as a part of a test-suite { int identityHashCode = System.identityHashCode(runNotifier); if (!notifierIdentities.contains(identityHashCode)) { addLogRunListener(runNotifier, identityHashCode); } } super.run(runNotifier); } private static synchronized void addLogRunListener(RunNotifier notifier, int identityHashCode) { if (notifierIdentities.contains(identityHashCode)) { return; } notifierIdentities.add(identityHashCode); notifier.addListener(new CdiTestSuiteRunner.LogRunListener()); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { return new ContainerAwareMethodInvoker(method, test); } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { currentTestRunner.set(this); TestControl testControl = method.getAnnotation(TestControl.class); ContainerAwareTestContext currentTestContext = new ContainerAwareTestContext(testControl, this.testContext); currentTestContext.applyBeforeMethodConfig(method.getMethod()); try { super.runChild(method, notifier); } finally { currentTestContext.applyAfterMethodConfig(); } } @Override protected Object createTest() throws Exception { BeanManager beanManager = BeanManagerProvider.getInstance().getBeanManager(); Class<?> type = getTestClass().getJavaClass(); Set<Bean<?>> beans = beanManager.getBeans(type); Object result; if (!USE_TEST_CLASS_AS_CDI_BEAN || beans == null || beans.isEmpty()) { result = super.createTest(); BeanProvider.injectFields(result); //fallback to simple injection } else { Bean<Object> bean = (Bean<Object>) beanManager.resolve(beans); CreationalContext<Object> creationalContext = beanManager.createCreationalContext(bean); result = beanManager.getReference(bean, type, creationalContext); } return result; } //TODO use Rules instead @Override protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { Statement result = super.withBefores(method, target, statement); result = wrapBeforeStatement(result, getTestClass(), target); return result; } private Statement wrapBeforeStatement(Statement statement, TestClass testClass, Object target) { for (TestStatementDecoratorFactory statementHandler : this.statementDecoratorFactories) { Statement result = statementHandler.createBeforeStatement(statement, testClass, target); if (result != null) { statement = result; } } return statement; } //TODO use Rules instead @Override protected Statement withAfters(FrameworkMethod method, final Object target, final Statement statement) { Statement result = super.withAfters(method, target, statement); result = wrapAfterStatement(result, getTestClass(), target); return result; } private Statement wrapAfterStatement(Statement statement, TestClass testClass, Object target) { for (TestStatementDecoratorFactory statementHandler : this.statementDecoratorFactories) { Statement result = statementHandler.createAfterStatement(statement, testClass, target); if (result != null) { statement = result; } } return statement; } @Override protected Statement withBeforeClasses(Statement statement) { return new BeforeClassStatement(super.withBeforeClasses(statement), this.testContext, getTestClass().getJavaClass()); } @Override protected Statement withAfterClasses(Statement statement) { Statement result = super.withAfterClasses(statement); if (!CdiTestSuiteRunner.isContainerStarted()) { return new AfterClassStatement(result, this.testContext); } return result; } //TODO use Rules instead @Override protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) { Statement result = super.withPotentialTimeout(method, test, next); if (result instanceof FailOnTimeout) { return new Statement() { @Override public void evaluate() throws Throwable { throw new RuntimeException("@" + Test.class.getName() + "#timeout isn't supported"); } }; } return result; } private class ContainerAwareMethodInvoker extends Statement { private final FrameworkMethod method; private final Object originalTarget; public ContainerAwareMethodInvoker(FrameworkMethod method, Object originalTarget) { this.method = method; this.originalTarget = originalTarget; } @Override public void evaluate() throws Throwable { BeanManager beanManager = BeanManagerProvider.getInstance().getBeanManager(); Class<?> type = this.method.getMethod().getDeclaringClass(); Set<Bean<?>> beans = beanManager.getBeans(type); if (!USE_TEST_CLASS_AS_CDI_BEAN || beans == null || beans.isEmpty()) { if (!ALLOW_INJECTION_POINT_MANIPULATION) { BeanProvider.injectFields(this.originalTarget); //fallback to simple injection } invokeMethod(this.originalTarget); } else { Bean<Object> bean = (Bean<Object>) beanManager.resolve(beans); CreationalContext<Object> creationalContext = beanManager.createCreationalContext(bean); Object target = beanManager.getReference(bean, type, creationalContext); try { invokeMethod(target); } finally { if (bean.getScope().equals(Dependent.class)) { bean.destroy(target, creationalContext); } } } } private void invokeMethod(Object target) { try { this.method.invokeExplosively(target); } catch (Throwable throwable) { throw ExceptionUtils.throwAsRuntimeException(throwable); } } } private class BeforeClassStatement extends Statement { private final Statement wrapped; private final ContainerAwareTestContext testContext; private final Class testClass; BeforeClassStatement(Statement statement, ContainerAwareTestContext testContext, Class testClass) { this.wrapped = statement; this.testContext = testContext; this.testClass = testClass; } @Override public void evaluate() throws Throwable { testContext.applyBeforeClassConfig(this.testClass); wrapped.evaluate(); } } private class AfterClassStatement extends Statement { private final Statement wrapped; private final ContainerAwareTestContext testContext; public AfterClassStatement(Statement statement, ContainerAwareTestContext testContext) { this.wrapped = statement; this.testContext = testContext; } @Override public void evaluate() throws Throwable { try { wrapped.evaluate(); } finally { testContext.applyAfterClassConfig(); } } } private static class ContainerAwareTestContext { private ContainerAwareTestContext parent; private final ProjectStage projectStage; private final TestControl testControl; private ProjectStage previousProjectStage; private boolean containerStarted = false; //only true for the layer it was started in private Stack<Class<? extends Annotation>> startedScopes = new Stack<Class<? extends Annotation>>(); private List<ExternalContainer> externalContainers; ContainerAwareTestContext(TestControl testControl, ContainerAwareTestContext parent) { this.parent = parent; Class<? extends ProjectStage> foundProjectStageClass; if (testControl == null) { this.testControl = new TestControlLiteral(); if (parent != null) { foundProjectStageClass = parent.testControl.projectStage(); } else { foundProjectStageClass = this.testControl.projectStage(); } } else { this.testControl = testControl; foundProjectStageClass = this.testControl.projectStage(); } this.projectStage = ProjectStage.valueOf(foundProjectStageClass.getSimpleName()); ProjectStageProducer.setProjectStage(this.projectStage); } boolean isContainerStarted() { return this.containerStarted || (this.parent != null && this.parent.isContainerStarted()) || CdiTestSuiteRunner.isContainerStarted(); } Class<? extends Handler> getLogHandlerClass() { return this.testControl.logHandler(); } void applyBeforeClassConfig(Class testClass) { CdiContainer container = CdiContainerLoader.getCdiContainer(); if (!isContainerStarted()) { if (!CdiTestSuiteRunner.isContainerStarted()) { container.boot(CdiTestSuiteRunner.getTestContainerConfig()); setContainerStarted(); bootExternalContainers(testClass); } } List<Class<? extends Annotation>> restrictedScopes = new ArrayList<Class<? extends Annotation>>(); //controlled by the container and not supported by weld: restrictedScopes.add(ApplicationScoped.class); restrictedScopes.add(Singleton.class); if (this.parent == null && this.testControl.getClass().equals(TestControlLiteral.class)) { //skip scope-handling if @TestControl isn't used explicitly on the test-class -> TODO re-visit it restrictedScopes.add(RequestScoped.class); restrictedScopes.add(SessionScoped.class); } startScopes(container, testClass, null, restrictedScopes.toArray(new Class[restrictedScopes.size()])); } private void bootExternalContainers(Class testClass) { if (!this.testControl.startExternalContainers()) { return; } if (this.externalContainers == null) { List<ExternalContainer> configuredExternalContainers = ServiceUtils.loadServiceImplementations(ExternalContainer.class); Collections.sort(configuredExternalContainers, new Comparator<ExternalContainer>() { @Override public int compare(ExternalContainer ec1, ExternalContainer ec2) { return ec1.getOrdinal() > ec2.getOrdinal() ? 1 : -1; } }); this.externalContainers = new ArrayList<ExternalContainer>(configuredExternalContainers.size()); ExternalContainer externalContainerBean; for (ExternalContainer externalContainer : configuredExternalContainers) { //needed to use cdi-observers in the container optionally externalContainerBean = BeanProvider.getContextualReference(externalContainer.getClass(), true); if (externalContainerBean != null) { this.externalContainers.add(externalContainerBean); } else { this.externalContainers.add(externalContainer); } } for (ExternalContainer externalContainer : this.externalContainers) { try { if (externalContainer instanceof TestAware) { ((TestAware)externalContainer).setTestClass(testClass); } externalContainer.boot(); } catch (RuntimeException e) { Logger.getLogger(CdiTestRunner.class.getName()).log(Level.WARNING, "booting " + externalContainer.getClass().getName() + " failed", e); } } } } void applyAfterClassConfig() { CdiContainer container = CdiContainerLoader.getCdiContainer(); stopStartedScopes(container); if (this.containerStarted) { if (CdiTestSuiteRunner.isStopContainerAllowed()) { shutdownExternalContainers(); container.shutdown(); //stop the container on the same level which started it CdiTestSuiteRunner.setContainerStarted(false); } } } private void shutdownExternalContainers() { if (this.externalContainers == null) { return; } for (ExternalContainer externalContainer : this.externalContainers) { try { externalContainer.shutdown(); } catch (RuntimeException e) { Logger.getLogger(CdiTestRunner.class.getName()).log(Level.WARNING, "shutting down " + externalContainer.getClass().getName() + " failed", e); } } } void applyBeforeMethodConfig(Method testMethod) { this.previousProjectStage = ProjectStageProducer.getInstance().getProjectStage(); ProjectStageProducer.setProjectStage(this.projectStage); setCurrentTestMethod(testMethod); startScopes(CdiContainerLoader.getCdiContainer(), testMethod.getDeclaringClass(), testMethod); } void applyAfterMethodConfig() { try { stopStartedScopes(CdiContainerLoader.getCdiContainer()); } finally { setCurrentTestMethod(null); ProjectStageProducer.setProjectStage(previousProjectStage); previousProjectStage = null; currentTestRunner.remove(); currentTestRunner.set(null); } } void setContainerStarted() { this.containerStarted = true; CdiTestSuiteRunner.setContainerStarted(true); } private void startScopes(CdiContainer container, Class testClass, Method testMethod, Class<? extends Annotation>... restrictedScopes) { try { automaticScopeHandlingActive.set(Boolean.TRUE); ContextControl contextControl = container.getContextControl(); List<Class<? extends Annotation>> scopeClasses = new ArrayList<Class<? extends Annotation>>(); Collections.addAll(scopeClasses, this.testControl.startScopes()); if (scopeClasses.isEmpty()) { addScopesForDefaultBehavior(scopeClasses); } else { List<TestControlValidator> testControlValidatorList = ServiceUtils.loadServiceImplementations(TestControlValidator.class); for (TestControlValidator testControlValidator : testControlValidatorList) { if (testControlValidator instanceof TestAware) { if (testMethod != null) { ((TestAware)testControlValidator).setTestMethod(testMethod); } ((TestAware)testControlValidator).setTestClass(testClass); } try { testControlValidator.validate(this.testControl); } finally { if (testControlValidator instanceof TestAware) { ((TestAware)testControlValidator).setTestClass(null); ((TestAware)testControlValidator).setTestMethod(null); } } } } for (Class<? extends Annotation> scopeAnnotation : scopeClasses) { if (this.parent != null && this.parent.isScopeStarted(scopeAnnotation)) { continue; } if (isRestrictedScope(scopeAnnotation, restrictedScopes)) { continue; } try { //force a clean context - TODO discuss onScopeStopped call contextControl.stopContext(scopeAnnotation); contextControl.startContext(scopeAnnotation); this.startedScopes.add(scopeAnnotation); onScopeStarted(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to start scope @" + scopeAnnotation.getName(), e); } } } finally { automaticScopeHandlingActive.remove(); automaticScopeHandlingActive.set(null); } } private void addScopesForDefaultBehavior(List<Class<? extends Annotation>> scopeClasses) { if (this.parent != null && !this.parent.isScopeStarted(RequestScoped.class)) { if (!scopeClasses.contains(RequestScoped.class)) { scopeClasses.add(RequestScoped.class); } } if (this.parent != null && !this.parent.isScopeStarted(SessionScoped.class)) { if (!scopeClasses.contains(SessionScoped.class)) { scopeClasses.add(SessionScoped.class); } } } private boolean isRestrictedScope(Class<? extends Annotation> scopeAnnotation, Class<? extends Annotation>[] restrictedScopes) { for (Class<? extends Annotation> restrictedScope : restrictedScopes) { if (scopeAnnotation.equals(restrictedScope)) { return true; } } return false; } private boolean isScopeStarted(Class<? extends Annotation> scopeAnnotation) { return this.startedScopes.contains(scopeAnnotation); } private void stopStartedScopes(CdiContainer container) { try { automaticScopeHandlingActive.set(Boolean.TRUE); while (!this.startedScopes.empty()) { Class<? extends Annotation> scopeAnnotation = this.startedScopes.pop(); //TODO check if context was started by parent try { container.getContextControl().stopContext(scopeAnnotation); onScopeStopped(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to stop scope @" + scopeAnnotation.getName(), e); } } } finally { automaticScopeHandlingActive.remove(); automaticScopeHandlingActive.set(null); } } private void onScopeStarted(Class<? extends Annotation> scopeClass) { List<ExternalContainer> externalContainerList = collectExternalContainers(this); for (ExternalContainer externalContainer : externalContainerList) { externalContainer.startScope(scopeClass); } } private void onScopeStopped(Class<? extends Annotation> scopeClass) { List<ExternalContainer> externalContainerList = collectExternalContainers(this); for (ExternalContainer externalContainer : externalContainerList) { externalContainer.stopScope(scopeClass); } } private static List<ExternalContainer> collectExternalContainers(ContainerAwareTestContext testContext) { List<ExternalContainer> result = new ArrayList<ExternalContainer>(); if (testContext.externalContainers != null) { result.addAll(testContext.externalContainers); } if (testContext.parent != null) { result.addAll(collectExternalContainers(testContext.parent)); } return result; } private void setCurrentTestMethod(Method testMethod) { List<ExternalContainer> externalContainerList = collectExternalContainers(this); for (ExternalContainer externalContainer : externalContainerList) { if (externalContainer instanceof TestAware) { try { ((TestAware)externalContainer).setTestMethod(testMethod); } catch (Throwable t) { //with a correct setup it shouldn't happen //TODO better handling for invalid constellations Assert.fail(t.getMessage()); } } } } } public static Boolean isAutomaticScopeHandlingActive() { return automaticScopeHandlingActive.get(); } public static List<ExternalContainer> getActiveExternalContainers() { CdiTestRunner cdiTestRunner = currentTestRunner.get(); if (cdiTestRunner == null || cdiTestRunner.testContext == null || cdiTestRunner.testContext.externalContainers == null) { return Collections.emptyList(); } return Collections.unmodifiableList(cdiTestRunner.testContext.externalContainers); } }